/*
 * EventGenerator.java
 *
 * Created on August 22, 2002, 3:47 PM
 */

package org.infoset.xml.sax;

import java.util.*;
import java.net.*;

import org.xml.sax.*;
import org.xml.sax.helpers.*;

import org.infoset.xml.*;
import org.xml.sax.ext.LexicalHandler;


/**
 *
 * @author  R. Alexander Milowski
 */
public class SAXEventGenerator extends DefaultHandler implements LexicalHandler {
   
   static class NSDecl {
      String prefix;
      URI namespace;
      NSDecl(String prefix,URI namespace) {
         this.prefix = prefix;
         this.namespace = namespace;
      } 
   }

   Infoset context;
   ItemConstructor constructor;
   ItemDestination dest;
   Locator locator;

   List prefixes;
   URI defaultNamespace;
   URI forcedBaseURI;
   List<URI> uriStack;
   List<Integer> uriLevels;
   int uriLevel;
   boolean unsetDefaultNamespace;
   char [] dataBuffer;
   int dataLength;
   int level;
   boolean inDTD;
 
   /**
    * Constructs a SAX-compliant handler for generating events.  The setInfoset and setItemDestination methods must be called in addition to properly initialize this object.
    *
    */
   public SAXEventGenerator() {
      this.context = InfosetFactory.getDefaultInfoset();
      this.constructor = context.createItemConstructor();
      this.dest = null;
      this.locator = null;
      this.prefixes = new ArrayList();
      this.defaultNamespace = null;
      this.dataBuffer = new char[1024];
      this.dataLength = 0;
      this.forcedBaseURI = null;
      this.level = -1;
      this.inDTD = false;
      this.uriStack = new ArrayList<URI>();
      this.uriLevels = new ArrayList<Integer>();
   }

   /**
    * Sets the context for event construction.
    */
   public void setItemConstructor(ItemConstructor constructor) {
      this.constructor = constructor;
      this.context = constructor.getInfoset();
   }
   
   /**
    * Sets the destination that will receive the events.
    */
   public void setItemDestination(ItemDestination dest) {
      this.dest = dest;
   }
   
   /**
    * Sets the locator to be used for handle entity locations.
    */
   public void setDocumentLocator(Locator locator) {
      //Logger.getAnonymousLogger().info("new Locator: "+locator);
      this.locator = locator;
   }
   
   public void setForcedBaseURI(URI uri) {
      forcedBaseURI = uri;
   }

   /**
    * A SAX method for handling the document.
    */
   public void startDocument() 
      throws SAXException
   {
      if (dest==null) {
         return;
      }
      try {
         // Construct a URI out of the base URI from the location.
         // TODO: will there always be a URI value?
         String sysid = locator==null ? null : locator.getSystemId();
         URI uri = null;
         if (sysid==null || sysid.length()==0) {
            uri = forcedBaseURI;
         } else {
            uri = sysid==null ? null : new URI(sysid);
         }

         // Reset namespace handling
         defaultNamespace = null;
         unsetDefaultNamespace = true;
         prefixes.clear();
         
         // Reset data handling
         dataLength = 0;
         
         if (level<0) {
            // Some XSLT processors (e.g. Saxon) call startDocument() more than once
            // While this is a bug, the above conditional takes care of that.
            
            //Logger.getAnonymousLogger().info("Base URI: "+uri);
            // Construct the document info item.
            Item document = constructor.createDocument(uri);
            // Send event
            dest.send(document);
            uriStack.add(uri);
            uriLevels.add(level);
            uriLevel = level;
         }
         level++;
         
      } catch (URISyntaxException ex) { 
         throw new SAXException("Invalid document URI "+ex.getInput(),ex);
      } catch (XMLException ex) { 
         throw new SAXException(ex.getMessage(),ex);
      }
   }

   /**
    * A SAX method for handling the document.
    */
   public void endDocument() 
      throws SAXException
   {
      level--;
      if (uriLevel==level) {
         uriStack.remove(uriStack.size()-1);
         uriLevels.remove(uriLevels.size()-1);
         uriLevel = -1;
      }
      if (dest==null) {
         return;
      }
      try {
         defaultNamespace = null;
         prefixes.clear();
         dest.send(constructor.createDocumentEnd());
      } catch (XMLException ex) {
         throw new SAXException(ex.getMessage(),ex);
      }
   }

   /**
    * A SAX method for handling the document.
    */
   public void startPrefixMapping(String prefix,String uri) 
      throws SAXException
   {
      //System.out.println("Prefix mapping: "+prefix+" -> "+uri);
      if (dest==null) {
         return;
      }
      try {
         
         // If no prefix, then it is a default namespace
         //System.out.println("prefix "+prefix+" -> "+uri);
         if (prefix==null || prefix.length()==0) {
            // If the uri is no specified, the default namespace is being "unset"
            if (uri==null || uri.length()==0) {
	            //System.out.println("default unset!");
               unsetDefaultNamespace = true;
               defaultNamespace = null;
            } else {
               unsetDefaultNamespace = false;
               defaultNamespace = context.createNamespace(uri);
            }
         } else {
            // Add the prefix to the list to be declared
            // TODO: need to optimize use of NSDecl
            prefixes.add(new NSDecl(prefix,context.createNamespace(uri)));
         }
      } catch (URISyntaxException ex) {
         // TODO: this should be reported as an error
         throw new SAXException("Invalid namespace URI "+ex.getInput(),ex);
      }
   }

   /**
    * A SAX method for handling the document.
    */
   public void startElement(String namespaceURI,String localName,String qName,Attributes atts) 
      throws SAXException
   {
      level++;
      if (dest==null) {
         return;
      }
      // Send pending data events if needed
      flushData(false);
      
      // Process element information
      
      String prefix = Name.NO_PREFIX;
      
      //System.out.println("localName='"+localName+"', namespaceURI='"+namespaceURI+"', qName='"+qName+"'");
      if (localName==null || localName.length()==0) {
         localName = qName;
      } else {
         int colon = qName.indexOf(':');
         if (colon>0) {
            prefix = qName.substring(0,colon);
         }
      }
      try {
         Name name = (namespaceURI==null || namespaceURI.length()==0) ?
            context.createName(localName) :
            context.createName(context.createNamespace(namespaceURI),localName);
         Element current = constructor.createElement(name);
         URI currentBase = uriStack.get(uriStack.size()-1);
         //System.out.println(name+": "+uriStack.size()+": current base URI: "+currentBase);
         current.setBaseURI(currentBase,true);
         if (locator!=null) {
            current.setLine(locator.getLineNumber());
            current.setColumn(locator.getColumnNumber());
         }
         
         current.setPrefix(prefix);

         // Process attributes
         
         int len = atts.getLength();
         for (int i=0; i<len; i++) {
            String alocalName = atts.getLocalName(i);
            String aqname = atts.getQName(i);
            String aNamespaceName = atts.getURI(i);
            if (alocalName.length()!=0 && !"http://www.w3.org/2000/xmlns/".equals(aNamespaceName)) {
               //System.out.println("Attribute: local='"+alocalName+"',namespace name='"+aNamespaceName+"'");
               name = (aNamespaceName==null || aNamespaceName.length()==0) ?
                  context.createName(alocalName) :
                  context.createName(context.createNamespace(aNamespaceName),alocalName);
               String value = atts.getValue(i);
               // TODO: handle type construction
               current.setAttributeValue(name,value);
               int colon = aqname.indexOf(':');
               if (colon>0) {
                  Attribute a = current.getAttributes().get(name);
                  a.setPrefix(aqname.substring(0,colon));
               }
            }
         }
         
         if (!current.isBaseURIInherited()) {
            URI newBase = current.getBaseURI();
            uriStack.add(newBase);
            uriLevels.add(level);
            uriLevel = level;
         }

         // Process namespace declarations scoped to this element.
         if (prefixes.size()!=0 || defaultNamespace!=null) {

            Iterator toDeclare = prefixes.iterator();
            while (toDeclare.hasNext()) {
               NSDecl nd = (NSDecl)toDeclare.next();
               current.addNamespaceBinding(nd.prefix,nd.namespace);
            }
            prefixes.clear();
            if (defaultNamespace!=null) {
               current.addNamespaceBinding(Name.NO_PREFIX,defaultNamespace);
               defaultNamespace = null;
            }
         }
         if (unsetDefaultNamespace) {
            current.addNamespaceBinding(Name.NO_PREFIX,Name.NO_NAMESPACE);
            unsetDefaultNamespace = false;
         }
         
         // Send info item event
         dest.send(current);

      } catch (URISyntaxException ex) { 
         // TODO: this should be reported as an error
         throw new SAXException("Invalid namespace URI "+ex.getInput(),ex);
      } catch (XMLException ex) {
         throw new SAXException(ex.getMessage(),ex);
      }
   }

   /**
    * A SAX method for handling the document.
    */
   public void endElement(String namespaceURI,String localName,String qName) 
      throws SAXException
   {
      level--;
      if (uriLevel==level) {
         //System.out.println("Removing URI "+uriStack.get(uriStack.size()-1));
         if (!uriStack.isEmpty()) {
            uriStack.remove(uriStack.size()-1);
            uriLevels.remove(uriLevels.size()-1);
            uriLevel = uriLevels.get(uriLevels.size()-1);
         }
      }
      if (dest==null) {
         return;
      }
      // Send pending data events if necessary
      flushData(false);
      
      // Send end-element event

      try {
         Name name = (namespaceURI==null || namespaceURI.length()==0) ?
            context.createName(localName) :
            context.createName(context.createNamespace(namespaceURI),localName);
         ElementEnd end = constructor.createElementEnd(name); 
         dest.send(end);
         if (locator!=null) {
            end.setLine(locator.getLineNumber());
            end.setColumn(locator.getColumnNumber());
         }
      } catch (URISyntaxException ex) { 
         // TODO: this should be reported as an error
         throw new SAXException("Invalid namespace URI "+ex.getInput(),ex);
      } catch (XMLException ex) {
         throw new SAXException(ex.getMessage(),ex);
      }
   }

   /**
    * A SAX method for handling the document.
    */
   public void characters(char [] ch,int start,int length) {
      if (inDTD) {
         return;
      }
      if (dest==null) {
         return;
      }
      // TODO: handle types
      //System.out.println("characters = '"+(new String(ch,start,length))+"'");
      if ((dataLength+length)>dataBuffer.length) {
         char [] newDataBuffer = new char[dataBuffer.length+(length>1024 ? length+1024 : 1024)];
         System.arraycopy(dataBuffer,0,newDataBuffer,0,dataLength);
         dataBuffer = newDataBuffer;
      }
      System.arraycopy(ch,start,dataBuffer,dataLength,length);
      dataLength += length;
   }
   
   /**
    * A SAX method for handling the document.
    */
   public void ignoreableWhitespace(char [] ch,int start,int length) {
      if (inDTD) {
         return;
      }
      if (dest==null) {
         return;
      }
      //System.out.println("ignoreable = '"+(new String(ch,start,length))+"'");
      // TODO: handle ignoreable whitespace flag
      characters(ch,start,length);
   }
   
   public void comment(char [] ch, int start, int length) 
      throws SAXException
   {
      if (inDTD) {
         return;
      }
      flushData(false);
      try {
         dest.send(constructor.createComment(ch,start,length));
      } catch (XMLException ex) {
         throw new SAXException(ex.getMessage(),ex);
      }
   }
   
   public void startCDATA() 
      throws SAXException
   {
      flushData(false);
   }
   
   public void endCDATA() 
      throws SAXException
   {
      flushData(true);
   }
   
   public void startEntity(String name) {
   }
   
   public void endEntity(String name) {
   }
   
   public void startDTD(String name, String publicId, String systemId) {
      inDTD = true;
   }
   
   public void endDTD() {
      inDTD = false;
   }
   
   void flushData(boolean cdata) 
      throws SAXException
   {
      try {
         if (dataLength!=0) {
            Characters c = (Characters)constructor.createCharacters(new String(dataBuffer,0,dataLength)); 
            c.setCDataSection(cdata);
            dest.send(c);
            if (locator!=null) {
               c.setLine(locator.getLineNumber());
               c.setColumn(locator.getColumnNumber());
            }
         }
         dataLength = 0;
      } catch (XMLException ex) {
         throw new SAXException(ex.getMessage(),ex);
      }
   }
   
   public void processingInstruction(String target,String data) 
      throws SAXException
   {
      if (inDTD) {
         return;
      }
      flushData(false);
      if (dest==null) {
         return;
      }
      try {
         dest.send(constructor.createProcessingInstruction(context.createName(target),data));
      } catch (XMLException ex) {
         throw new SAXException(ex.getMessage(),ex);
      }
   }
   

}
